#!/bin/bash

# Hard dependencies:
# - bash
# - inotifywait
# - find
# - pidof
# - killall

# All other dependencies are optional:
#
# - xrdb (for Xresources), dmenu, conky, tint2, openbox & xmlstarlet, geany
# If the above executables are not found in PATH the according section is skipped.
#
# - sed: replace color_scheme in geany.conf

############ USER CONFIG BEGIN #############
#
tmpdir=/tmp # no spaces or funny characters! You have been warned! (see trap further down)
tmpdir_cleanup=1 # set to 0 to keep logs

# let's call these **subthemes**
# They refer to subdirectories of the current theme directory
# setting the "boolean" vars to 1 means the script will try its best to find
# config files & take appropiate action.
# Anything other than 1 means FALSE!

# A subdir called conky, relevant files must include the string conky
conky=1
# For those that use a different conky executable:
conky_cmd=conky
conky_fallback=( "$HOME/.config/conky/sys-vert.conkyrc" "$HOME/.config/conky/journal.conkyrc" )
# how long to wait before conky(s) is/are started, in s:
conkydelay=3
# A subdir called dmenu, the only relevant file must be executable and called dmenu
dmenu=1
dmenu_fallback="$HOME/.config/dmenu/dmenu_fallback"
# A subdir called unst, the only relevant file must be called dunstrc
dunst=1
# A subdir called geany, relevant files (a color scheme) must end in conf
geany=1
# A subdir called openbox-3, the only relevant file must be called themerc
openbox=1
# For example LXDE users might need to change this
ob_rc="${XDG_CONFIG_HOME-"$HOME/.config"}/openbox/rc.xml"
# A subdir called tint2, relevant files must end in tint2rc
tint2=1
tint2_fallback=("$HOME/.config/tint2/tint2rc")
# A subdir called Xresources, relevant files must end in .xres
xresources=1
#
############ USER CONFIG END #############

me="${0##*/}"

configdir="${XDG_CONFIG_HOME-"$HOME/.config"}/$me"
mkdir -p "$configdir"

source "$configdir/config"

conky_restarter_pid=nothing

# deliberately test without quotes, because I don't know how to use variables
# with spaces in trap
! [ -d $tmpdir ] && echo "$tmpdir is not a valid temporary directory, exiting." && exit 1
# OK. From now on:
tmpdir="$tmpdir/$me"
mkdir -p "$tmpdir"

[[ "$tmpdir_cleanup" == 1 ]] && trap "rm -r $tmpdir;killall $me" EXIT || trap "killall $me" EXIT

notify_file="$tmpdir/notifications"
notify() {
    echo "$1" >> "$notify_file"
    echo -e "$1"
}
files() {
    # simply checks if these files exist, because 'test -r' can only check for 1 file
    # mywiki.wooledge.org/BashFAQ/004
    shopt -s nullglob dotglob
    files=($@)
    (( ${#files[*]} )) && return 0 || return 1
}
conky_restarter() {
    if grep '^[[:space:]]*background[[:space:]]=[[:space:]]true' "$1"; then
    # If your conky forks to the background, just start it normally and drop the respawn
    "$conky_cmd" -c "$config" & return
    fi
    log="$tmpdir/${file##*/}.log"
    restarts=0
    while :; do
    time="$SECONDS"
    "$conky_cmd" -c "$1" >>"$log" 2>&1
    printf "\nConky apparently crashed at %($TIMESTAMP_FORMAT)T\n\n" >> "$log"
    ((restarts++))
    time=$(( SECONDS - time ))
    if (( time < 10 )) && (( restarts > 10 )); then
      printf "Too many restarts and less than 10s between start and crash - not good. Exiting." >> "$log"
      return
    fi
    sleep 1
    done
}
strip_ansi() {
    shopt -s extglob # function uses extended globbing
    while read -r line; do
        echo "${line//$'\e'\[*([0-9;])m/}"
    done
}

# Works only on systems that have GTK3 rc files.
# Should Work (tm) on LXDE, LXQt and custom desktops
# If however you set your theme with e.g. XFCE4's Settings dialog, no such
# rc files are created.
watchfile="${XDG_CONFIG_HOME-"$HOME/.config"}/gtk-3.0/settings.ini"
[ ! -r "$watchfile" ] && echo "No readable gtk settings file to watch. Exiting." && exit 1

[ ! -r "$ob_rc" ] && openbox=0 && unset ob_rc

geany_dir="${XDG_CONFIG_HOME-"$HOME/.config"}/geany"
[ -d "$geany_dir" ] && [ -x "$geany_dir" ] || { geany=0; unset geany_dir; }

# for each subtheme: the first folder found wins.
# That way we can extend & override system themes in $HOME
themepath=( "$HOME/.local/share/themes" "/usr/share/themes" )

# will be set to 0 at the end of the first loop
firstrun=1

while :; do
    
    rm -f "$notify_file"
    CURRENT_THEME="$(/usr/bin/grep gtk-theme-name "$watchfile")"
    CURRENT_THEME="${CURRENT_THEME##*=}"
    CURRENT_THEME="${CURRENT_THEME#*\"}"
    CURRENT_THEME="${CURRENT_THEME%\"*}"
    
    if [[ "x$CURRENT_THEME" == "x" ]]; then
        notify "Could not extract current theme name from $watchfile."
    else
        echo "$CURRENT_THEME" > "$configdir/current_theme"
        for path in "${themepath[@]}"; do
            path="$path/$CURRENT_THEME/gtk-2.0/gtkrc"
            if [ -r "$path" ]; then
                file="$configdir/gtk2-color-scheme"
                echo "$CURRENT_THEME" > "$file.current"
                grep gtk.color.scheme "$path" | cut -d\" -f2 | sed 's/\\n/\n/g' >> "$file.current"
                cp "$file.current" "$file.$CURRENT_THEME"
                break
            fi
        done
        # lxappearance uses the folder name to name the theme, NOT what is in
        # index.theme (and both gtk2 and gtk3 recognize it), so we just assume that
        # CURRENT_THEME is the actual name of the theme directory
        if [[ "$conky" == "1" ]] && which "$conky_cmd" >/dev/null; then
            sleep "$conkydelay"
            done=0
            for path in "${themepath[@]}"; do
                path="$path/$CURRENT_THEME/conky"
                if [ -d "$path" ] && [ -x "$path" ] && files "$path"/*conky*; then
                    done=1
                    killall "$conky_cmd"
                    # make sure it's 10x dead:
                    for ((i=0;i<10;i++)); do
                        sleep 0.1
                        pidof "$conky_cmd" >/dev/null || break
                        killall -9 "$conky_cmd"
                    done
                    for file in "$path"/*conky*; do
                        sleep 0.1
                        "$conky_cmd" -c "$file" > "$tmpdir/${file##*/}".log 2>&1 &
                        notify "Started $conky_cmd -c $file"
                    done
                    break # because the first path wins
                fi
            done
            # fallback scenario
            if [[ "$done" == 0 ]] && (( ${#conky_fallback[@]} > 0 )); then
                killall -q "$conky_cmd"
                # make sure it's 10x dead:
                for ((i=0;i<10;i++)); do
                    sleep 0.1
                    pidof "$conky_cmd" >/dev/null || break
                    killall -9 "$conky_cmd"
                done
                for file in "${conky_fallback[@]}"; do
                    "$conky_cmd" -c "$file" > "$tmpdir/${file##*/}".log 2>&1 &
                    notify "Started $conky_cmd -c $file"
                done
            fi
        fi & conkypid=$!
        
        if [[ "$dmenu" == "1" ]] && [[ "$firstrun" == 0 ]]; then
            done=0
            for path in "${themepath[@]}"; do
                path="$path/$CURRENT_THEME/dmenu"
                if [ -d "$path" ] && [ -x "$path" ] && [ -x "$path"/dmenu ]; then
                    [ -e "$HOME/bin/dmenu" ] && \
                    [[ "$(file -b --mime-type "$HOME/bin/dmenu")" != *symlink* ]] && \
                    mv "$HOME/bin/dmenu" "$HOME/bin/dmenu.$me.bak"
                    ln -sf "$path/dmenu" $HOME/bin/dmenu
                    notify "Symlinked $path/dmenu to $HOME/bin"
                    done=1
                    break # because the first path wins
                fi
            done
            # fallback scenario
            if [[ "$done" == 0 ]] && [ -n "$dmenu_fallback" ]; then
                [ -e "$HOME/bin/dmenu" ] && \
                [[ "$(file -b --mime-type "$HOME/bin/dmenu")" != *symlink* ]] && \
                mv "$HOME/bin/dmenu" "$HOME/bin/dmenu.$me.bak"
                ln -sf "$dmenu_fallback" $HOME/bin/dmenu
                notify "Symlinked $dmenu_fallback to $HOME/bin"
            fi
        fi & dmenupid=$!
        
        if [[ "$dunst" == "1" ]] && [[ "$firstrun" == 0 ]]; then
            for path in "${themepath[@]}"; do
                path="$path/$CURRENT_THEME/dunst"
                if [ -d "$path" ] && [ -x "$path" ] && [ -r "$path"/dunstrc ]; then
                    dest="${XDG_CONFIG_HOME-"$HOME/.config"}/dunst/dunstrc"
                    [ -e "$dest" ] && \
                    [[ "$(file -b --mime-type "$dest")" != *symlink* ]] && \
                    mv "$dest" "$dest.$me.bak"
                    ln -sf "$path/dunstrc" $dest
                    killall -q dunst
                    notify "Symlinked $path/dunstrc to $dest"
                    break # because the first path wins
                fi
            done
        fi & dunstpid=$!
        
        if [[ "$geany" == "1" ]] && [[ "$firstrun" == 0 ]]; then
            for path in "${themepath[@]}"; do
                path="$path/$CURRENT_THEME/geany"
                if [ -d "$path" ] && [ -x "$path" ] && files "$path"/*conf; then
                    mkdir -p "$geany_dir/colorschemes"
                    schemefile=""
                    for file in "$path"/*conf; do
                        schemefile="${me}_${CURRENT_THEME}_${file##*/}"
                        ln -s "$file" "$geany_dir/colorschemes/$schemefile"
                        notify "Symlinked $schemefile to $geanydir/colorschemes"
                        if [ -w "$geany_dir/geany.conf" ] && which sed >/dev/null; then
                            cp -f "$geany_dir/geany.conf" "$geany_dir/geany.conf.$me"
                            sed -i "s/^color_scheme=.*$/color_scheme=$schemefile/" "$geany_dir/geany.conf"
                            notify "Set color_scheme=$schemefile in $geany_dir/geany.conf.\nNot effective while geany is running."
                        fi
                    done
                    if [[ "$schemefile" != "" ]] && [ -w "$geany_dir/geany.conf" ] && which sed >/dev/null; then
                        cp -f "$geany_dir/geany.conf" "$geany_dir/geany.conf.$me"
                        sed -i "s/^color_scheme=.*$/color_scheme=$schemefile/" "$geany_dir/geany.conf"
                    fi
                    break # because the first path wins
                fi
            done
        fi & geanypid=$!
        
        if [[ "$openbox" == "1" ]] && [[ "$firstrun" == 0 ]] && which openbox >/dev/null && which xmlstarlet >/dev/null; then
            for path in "${themepath[@]}"; do
                path="$path/$CURRENT_THEME/openbox-3"
                if [ -d "$path" ] && [ -x "$path" ] && [ -r "$path"/themerc ]; then
                    cp -f "$ob_rc" "$ob_rc"."$me"
                    # priceless piece of information: https://superuser.com/a/508143
                    xmlstarlet ed -L -N o="http://openbox.org/3.4/rc" -u '/o:openbox_config/o:theme/o:name' -v "$CURRENT_THEME" "$ob_rc"
                    pidof openbox >/dev/null && sleep 1 && openbox --reconfigure
                    notify "Set openbox theme to $CURRENT_THEME"
                    break # because the first path wins
                fi
            done
        fi & obpid=$!
        if [[ "$xresources" == "1" ]] && which xrdb >/dev/null; then
            for path in "${themepath[@]}"; do
                path="$path/$CURRENT_THEME/Xresources"
                if [ -d "$path" ] && [ -x "$path" ] && files "$path"/*xres; then
                    while pidof xrdb >/dev/null; do sleep 0.1; done
                    for file in "$path"/*xres; do
                        xrdb -merge "$file"
                        notify "Merged $file into Xrdb"
                    done
                    break # because the first path wins
                fi
            done
        fi & xrespid=$!
        if [[ "$tint2" == "1" ]] && which tint2 >/dev/null; then
            done=0
            sleep 2 # otherwise tint2 triggers a restart because "configuration change
                    # in the root window" - no idea why, I tested thoroughly
            for path in "${themepath[@]}"; do
                path="$path/$CURRENT_THEME/tint2"
                if [ -d "$path" ] && [ -x "$path" ] && files "$path"/*tint2rc; then
                    killall tint2 >/dev/null && sleep 0.1
                    for file in "$path"/*tint2rc; do
                        tint2 -c "$file" 2>&1 | strip_ansi > "$tmpdir/${file##*/}".log &
                        notify "Started tint2 -c $file"
                    done
                    done=1
                    break # because the first path wins
                fi
            done
            # fallback scenario
            if [[ "$done" == 0 ]] && (( ${#tint2_fallback[@]} > 0 )); then
                killall tint2 >/dev/null && sleep 0.1
                for file in "${tint2_fallback[@]}"; do
                    tint2 -c "$file" 2>&1 | strip_ansi > "$tmpdir/${file##*/}".log &
                    notify "Started tint2 -c $file"
                done
            fi
        fi & tint2pid=$!
    fi
    wait $conkypid $dmenupid $dunstpid $geanypid $obpid $tint2pid $xrespid
    echo "Waited for $conkypid $dmenupid $dunstpid $geanypid $obpid $tint2pid $xrespid"
    
    ((firstrun<1)) && notify-send -t 0 "$me" "$(< "$notify_file")"
    sleep 1
    echo "Waiting for changes..."
    inotifywait -e modify "$watchfile"
    firstrun=0
done
